
#include <openssl/evp.h>
#include <apr-1/apr_strings.h>
#include "AESSymmetricKey.h"
#include "../aprtools/APRPRNG.h"
#include "../utils/Hash.h"
#include "../utils/BaseConvert.h"
#include "../utils/StringUtils.h"

/** Constructor for a blank AES key. */
AESSymmetricKey::AESSymmetricKey()
{
   keypassword = NULL;
   keyname = NULL;
}

/** Constructor for a blank AES key, with a given name.
 * 
 * @param name
 * Name for the blank key. */
AESSymmetricKey::AESSymmetricKey(const char *name)
{
   keypassword = NULL;
   keyname = NULL;
   setKeyName(name);
}

/** Constructor for the generation of a new AES symmetric key.
 * 
 * @param bits
 * Length of the new key in bits. Should at least be a multiple of 8.
 * 
 * @param name
 * Name of the new key. */
AESSymmetricKey::AESSymmetricKey(int bits, const char *name)
{
   keypassword = NULL;
   APRPRNG aprrng;
   char *randomdata = aprrng.getRandomBytes(bits / 8);
   keypassword = BaseConvert::convertBytesToBase64(randomdata, bits / 8);
   free(randomdata);
   keyname = NULL;
   setKeyName(name);
}

/** Copy constructor for an AES symmetric key. Copies both the key and the
 * name.
 * 
 * @param other
 * Other key to copy into this new key. */
AESSymmetricKey::AESSymmetricKey(const AESSymmetricKey &other)
{
   if (other.keypassword)
   {
      keypassword = (char *)strdup(other.keypassword);
   } else keypassword = NULL;
   keyname = NULL;
   setKeyName(other.keyname);
}

/** Copy assignment operator for the AES symmetric key.
 * 
 * @param other
 * Other key to copy into this new key.
 * 
 * @return
 * Itself. */
AESSymmetricKey &AESSymmetricKey::operator=(const AESSymmetricKey &other)
{
   if (other.keypassword)
   {
      keypassword = (char *)strdup(other.keypassword);
   } else keypassword = NULL;
   keyname = NULL;
   setKeyName(other.keyname);
   return *this;
}

/** Destroys the key data (password) and the encapsulating class. */
AESSymmetricKey::~AESSymmetricKey()
{
   int len = strlen(keypassword);
   memset(keypassword, 0, len);
   free(keypassword);
   if (keyname) free((void *)keyname);
   keyname = NULL;
}

/** Obtains a cryptlib context into which the key is loaded.
 * 
 * @param[out] keycontext
 * A pointer to a cryptlib CRYPT_CONTEXT which is to be initialised with the
 * key.
 * 
 * @return
 * Zero on success, non-zero on failure. */
int AESSymmetricKey::getCryptlibContext(CRYPT_CONTEXT *keycontext)
{
   int nobytes = 0;
   char *binarydata = NULL;
   int status;
   int magictriescntxt = 0;
   do
   {
      status = cryptCreateContext(keycontext, CRYPT_UNUSED, CRYPT_ALGO_AES);
      if (status == CRYPT_ERROR_TIMEOUT) apr_sleep(rand() % CRYPT_MAX_TIMESLEEP);
      magictriescntxt++;
   } while ((status == CRYPT_ERROR_TIMEOUT) && (magictriescntxt < CRYPT_MAX_RETRIES));
   fCryptCheckError(status);
   if (status != CRYPT_OK) goto failover;
   status = cryptSetAttributeString(*keycontext, CRYPT_CTXINFO_LABEL, keyname, strlen(keyname));
   fCryptCheckError(status);
   if (status != CRYPT_OK) goto failover;
   status = cryptSetAttribute(*keycontext, CRYPT_CTXINFO_KEYSIZE, AES_BITS / 8);
   fCryptCheckError(status);
   nobytes = strlen(keypassword);
   binarydata = BaseConvert::convertBase64ToBytes(keypassword, &nobytes);
   status = cryptSetAttributeString(*keycontext, CRYPT_CTXINFO_KEY, binarydata, nobytes);
   fCryptCheckError(status);
   free(binarydata);
   if (status != CRYPT_OK) goto failover;
   return 0;
failover:
   return -1;
}

/** Obtains a cryptlib context meant particularly for encryption.
 * 
 * @param[out] keycontext
 * A pointer to a cryptlib CRYPT_CONTEXT which is to be initialised with the
 * key.
 * 
 * @return
 * Zero on success, non-zero on failure. */
int AESSymmetricKey::getCryptlibEncryptionContext(CRYPT_CONTEXT *keycontext)
{
   return getCryptlibContext(keycontext);
}

/** Obtains a cryptlib context meant particularly for decryption.
 * 
 * @param[out] keycontext
 * A pointer to a cryptlib CRYPT_CONTEXT which is to be initialised with the
 * key.
 * 
 * @return
 * Zero on success, non-zero on failure. */
int AESSymmetricKey::getCryptlibDecryptionContext(CRYPT_CONTEXT *keycontext)
{
   return getCryptlibContext(keycontext);
}

/** Import an AES key from a bespoke plain text representation and name it.
 * 
 * @param text
 * Text string which contains the encoded key.
 * 
 * @param name
 * The name to use for the key once imported.
 * 
 * @return
 * Zero if the import process succeeded, non-zero on failure. */
int AESSymmetricKey::importKeyFromASCII(const char *text, const char *name)
{
   if ((text == NULL) || (strlen(text) == 0))
   {
      errprint("cannot import a key from %s string", (text) ? "an empty" : "a NULL");
      return -1;
   }
   if ((name == NULL) || (strlen(name) == 0))
   {
      errprint("cannot import a key which has a %s name", (name) ? "an empty" : "a NULL");
      return -1;
   }
   if (text[0] == '-')
   {
      errprint("the text key looks like PEM data - wrong format");
      return -1;
   }
   char *remainder = NULL;
   char *duptext = strdup(text);
   const char *token = apr_strtok(duptext, "-", &remainder);
   if (strcmp(token, "AES_SYMM_KEY") != 0)
   {
      errprint("the text key does not have the magic token AES_SYMM_KEY, token is %s", token);
      free(duptext);
      return -1;
   }
   const char *tokentext = apr_strtok(NULL, "\n", &remainder);
   // Freeing parts goes first
   if (keypassword)
   {
      // Password has to burn
      memset(keypassword, 0, strlen(keypassword));
      free(keypassword);
   }
   keypassword = NULL;
   setKeyName(name);
   keypassword = (char *)malloc(sizeof(char)*(strlen(tokentext)+1));
   strcpy(keypassword, tokentext);
   free(duptext);
   return 0;
}

/** Import an AES key from some encrypted and passworded bespoke text format
 * and name it.
 * 
 * @param text
 * Text string which contains the encoded key.
 * 
 * @param password
 * Password to decrypt the encryption on the key.
 * 
 * @param name
 * Name for the key once imported.
 * 
 * @return
 * Zero on success, non-zero on failure. */
int AESSymmetricKey::importKeyFromASCIIWithPassword(const char *text, const char *password, const char *name)
{
   if ((text == NULL) || (strlen(text) == 0))
   {
      errprint("cannot import a key from %s string", (text) ? "an empty" : "a NULL");
      return -1;
   }
   if ((name == NULL) || (strlen(name) == 0))
   {
      errprint("cannot import a key which has a %s name", (name) ? "an empty" : "a NULL");
      return -1;
   }
   if (text[0] == '-')
   {
      errprint("the text key looks like PEM data - wrong format");
      return -1;
   }
   char *remainder = NULL;
   char *duptext = strdup(text);
   const char *token = apr_strtok(duptext, "-", &remainder);
   if (strcmp(token, "AES_SYMM_KEY") != 0)
   {
      errprint("the text key does not have the magic token AES_SYMM_KEY, token is %s", token);
      return -1;
   }
   const char *tokentext = apr_strtok(NULL, "\n", &remainder);
   if ((password == NULL) || (strlen(password) == 0))
   {
      int retval = importKeyFromASCII(tokentext, name);
      free(duptext);
      return retval;
   }
   // Freeing parts goes first
   if (keypassword)
   {
      // Password has to burn
      memset(keypassword, 0, strlen(keypassword));
      free(keypassword);
   }
   keypassword = NULL;
   setKeyName(name);
   // This means that in practice the name of the actor is also part of the key,
   // just not a part that has to be kept secret!
   EVP_CIPHER_CTX e_cntxt;
   const EVP_CIPHER *pwd_cipher_struct = EVP_des_ede3_cbc();
   char *finaliv = NULL;
   int bytes = 0;
#ifdef __USE_NAMES_AS_IVS
   char *sha1hash_hex = Hash::getSHA1Hash(name, strlen(name));
   bytes = strlen(sha1hash_hex);
   char *sha1hash = BaseConvert::convertHexadecimalToBytes(sha1hash_hex, &bytes);
   if (EVP_CIPHER_iv_length(pwd_cipher_struct) > ((strlen(sha1hash_hex) / 2) + strlen(INITIALISATION_VECTOR)))
   {
      errprint("initialisation vector too long - import failed");
   } else if (EVP_CIPHER_iv_length(pwd_cipher_struct) > (strlen(sha1hash_hex) / 2))
   {
      errprint("padding SHA1 sum to length of initialisation vector");
      finaliv = (char *)malloc(sizeof(char)*(EVP_CIPHER_iv_length(pwd_cipher_struct)));
      int idxpos = 0;
      memcpy(&(finaliv[idxpos]), sha1hash, strlen(sha1hash_hex) / 2);
      idxpos += (strlen(sha1hash_hex) / 2);
      memcpy(&(finaliv[idxpos]), INITIALISATION_VECTOR,
         EVP_CIPHER_iv_length(pwd_cipher_struct) - idxpos);
   } else
   {
      finaliv = (char *)malloc(sizeof(char)*(EVP_CIPHER_iv_length(pwd_cipher_struct)));
      memcpy(finaliv, sha1hash, EVP_CIPHER_iv_length(pwd_cipher_struct));
   }
   free(sha1hash_hex);
   free(sha1hash);
#else
   memcpy(finaliv, INITIALISATION_VECTOR, EVP_CIPHER_iv_length(pwd_cipher_struct));
#endif
   unsigned char iv[EVP_MAX_IV_LENGTH], key[EVP_MAX_KEY_LENGTH];
   EVP_BytesToKey(pwd_cipher_struct, EVP_sha1(), (const unsigned char *)"Monosodium_Glutamate", (const unsigned char *)password, strlen(password), 1, key, iv);

   EVP_CIPHER_CTX_init(&e_cntxt);
   // Decryption selected by last zero
   EVP_CipherInit_ex(&e_cntxt, pwd_cipher_struct, NULL, key, iv, 0);
   // Decode from base64
   bytes = strlen(tokentext);
   unsigned char *finalkeyencdbinary = (unsigned char *)BaseConvert::convertBase64ToBytes(tokentext, &bytes);
   // This isn't really a key, just a password entropically indistinguishable from a key,
   // so it can be any size within reason
   int keysize = strlen(tokentext)*3;
   unsigned char *keyplace = (unsigned char *)malloc(sizeof(unsigned char)*keysize*2);
   int thislength = keysize;
   // Block cipher, so add text
   EVP_CipherUpdate(&e_cntxt, keyplace, &thislength, finalkeyencdbinary, bytes);
   int thislength2 = keysize;
   EVP_CipherFinal_ex(&e_cntxt, &(keyplace[thislength]), &thislength2);
   EVP_CIPHER_CTX_cleanup(&e_cntxt);
   bytes = thislength + thislength2;
   char *final_base64 = BaseConvert::convertBytesToBase64((const char *)keyplace, bytes);
   keypassword = final_base64;
   free(finalkeyencdbinary);
   free(keyplace);
   free(duptext);
   return 0;
}

/** Export an AES key to a bespoke plain text representation.
 * 
 * @param[out] text
 * A pointer to the place to the put the text pointer. The responsibility to
 * free the text rests with the caller.
 * 
 * @return
 * Zero on success, non-zero on failure. */
int AESSymmetricKey::exportKeyToASCII(char **text)
{
   if (text == NULL)
   {
      errprint("can't put a new point in a NULL pointer!");
      return -1;
   }
   *text = StringUtils::varprintf("AES_SYMM_KEY-%s", keypassword);
   return 0;
}

/** Export an AES key to some encrypted and passworded bespoke text format.
 * 
 * @param[out] text
 * A pointer to the place to the put the text pointer. The responsibility to
 * free the text rests with the caller.
 * 
 * @param password
 * The password to use to encrypt the key.
 * 
 * @return
 * Zero on success, non-zero on failure. */
int AESSymmetricKey::exportKeyToASCIIWithPassword(char **text, const char *password)
{
   if (text == NULL)
   {
      errprint("can't put a new point in a NULL pointer!");
      return -1;
   }
   EVP_CIPHER_CTX e_cntxt;
   const EVP_CIPHER *pwd_cipher_struct = EVP_des_ede3_cbc();
   char *finaliv = NULL;
#ifdef __USE_NAMES_AS_IVS
   char *sha1hash_hex = Hash::getSHA1Hash(name, strlen(name));
   int bytes = strlen(sha1hash_hex);
   char *sha1hash = BaseConvert::convertHexadecimalToBytes(sha1hash_hex, &bytes);
   if (EVP_CIPHER_iv_length(pwd_cipher_struct) > ((strlen(sha1hash_hex) / 2) + strlen(INITIALISATION_VECTOR)))
   {
      errprint("initialisation vector too long - import failed");
   } else if (EVP_CIPHER_iv_length(pwd_cipher_struct) > (strlen(sha1hash_hex) / 2))
   {
      errprint("padding SHA1 sum to length of initialisation vector");
      finaliv = (char *)malloc(sizeof(char)*(EVP_CIPHER_iv_length(pwd_cipher_struct)));
      int idxpos = 0;
      memcpy(&(finaliv[idxpos]), sha1hash, strlen(sha1hash_hex) / 2);
      idxpos += (strlen(sha1hash_hex) / 2);
      memcpy(&(finaliv[idxpos]), INITIALISATION_VECTOR,
         EVP_CIPHER_iv_length(pwd_cipher_struct) - idxpos);
   } else
   {
      finaliv = (char *)malloc(sizeof(char)*(EVP_CIPHER_iv_length(pwd_cipher_struct)));
      memcpy(finaliv, sha1hash, EVP_CIPHER_iv_length(pwd_cipher_struct));
   }
   free(sha1hash_hex);
   free(sha1hash);
#else
   memcpy(finaliv, INITIALISATION_VECTOR, EVP_CIPHER_iv_length(pwd_cipher_struct));
#endif
   unsigned char iv[EVP_MAX_IV_LENGTH], key[EVP_MAX_KEY_LENGTH];
   EVP_BytesToKey(pwd_cipher_struct, EVP_sha1(), (const unsigned char *)"Monosodium_Glutamate", (const unsigned char *)password, strlen(password), 1, key, iv);

   EVP_CIPHER_CTX_init(&e_cntxt);
   // Encryption selected by last one
   EVP_CipherInit_ex(&e_cntxt, pwd_cipher_struct, NULL, key, iv, 1);
   // Key size should be big
   int keysize = strlen(keypassword)*3;
   unsigned char *keyplace = (unsigned char *)malloc(sizeof(unsigned char)*keysize*2);
   int thislength = keysize;
   int bytes = strlen(keypassword);
   unsigned char *binarypassword = (unsigned char *)BaseConvert::convertBase64ToBytes(keypassword, &bytes);
   // Block cipher, so add text
   EVP_CipherUpdate(&e_cntxt, keyplace, &thislength, binarypassword, bytes);
   int thislength2 = keysize;
   EVP_CipherFinal_ex(&e_cntxt, &(keyplace[thislength]), &thislength2);
   EVP_CIPHER_CTX_cleanup(&e_cntxt);
   bytes = thislength + thislength2;
   char *base64version = BaseConvert::convertBytesToBase64((const char *)keyplace, bytes);
   base64version[bytes] = '\0';
   *text = StringUtils::varprintf("AES_SYMM_KEY-%s", base64version);
   return 0;
}

/** Compare this key to another for equality.
 * 
 * @param symkey
 * Other SymmetricKey for comparison.
 * 
 * @return
 * Returns one for equal, zero for unequal and -1 for an error. */
int AESSymmetricKey::compareKeyEqual(SymmetricKey *symkey)
{
   if (symkey->getKeyType() != getKeyType())
   {
      errprint("cannot compare keys, this key is of type %s and the compared key is %s",
         getKeyTypeFullName(), symkey->getKeyTypeFullName());
      return -1;
   }
   char *keyexport = NULL;
   int retval = symkey->exportKeyToASCII(&keyexport);
   if (retval != 0)
   {
      errprint("failed to get other text key '%s' when comparing to this key '%s'", symkey->getKeyName(), getKeyName());
      return -1;
   }
   char *keyexport2 = NULL;
   int retval2 = exportKeyToASCII(&keyexport2);
   if (retval2 != 0)
   {
      errprint("failed to get this text key '%s' when comparing to other key '%s'", getKeyName(), symkey->getKeyName());
      return -1;
   }
   if (strcmp(keyexport, keyexport2) == 0) return 1;
   return 0;
}

/** Get the key type flag, in this case KEY_TYPE_SYMM_AES.
 * 
 * @return
 * Key type Key::KEY_TYPE_SYMM_AES. */
int AESSymmetricKey::getKeyType()
{
   return KEY_TYPE_SYMM_AES;
}
